/*
 * Copyright 2014 Frakbot (Sebastiano Poggi and Francesco Pontillo)
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.supply.dl.widget.span;

import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;

import java.lang.ref.WeakReference;

/**
 * Provides "jumping beans" functionality for a TextView.
 * Remember to call the {@link #stopJumping()} method once you're done
 * using the JumpingBeans (that is, when you detach the TextView from
 * the view tree, you hide it, or the parent Activity/Fragment goes in
 * the paused status). This will allow to release the animations and
 * free up memory and CPU that would be otherwise wasted.
 * Please note that you:
 * <ul>
 * <li><b>Must not</b> try to change a jumping beans text in a textview before calling
 * {@link #stopJumping()} as to avoid unnecessary invalidation calls;
 * the JumpingBeans class cannot know when this happens and will keep
 * animating the textview (well, try to, anyway), wasting resources</li>
 * <li><b>Must not</b> try to use a jumping beans text in another view; it will not
 * animate. Just create another jumping beans animation for each new
 * view</li>
 * <li><b>Must not</b> use more than one JumpingBeans instance on a single TextView, as
 * the first cleanup operation called on any of these JumpingBeans will also cleanup
 * all other JumpingBeans' stuff. This is most likely not what you want to happen in
 * some cases.</li>
 * <li><b>Should not</b> use JumpingBeans on large chunks of text. Ideally this should
 * be done on small views with just a few words. We've strived to make it as inexpensive
 * as possible to use JumpingBeans but invalidating and possibly relayouting a large
 * TextView can be pretty expensive.</li>
 * </ul>
 */
public final class JumpingBeans
{

	/**
	 * The default fraction of the whole animation time spent actually animating.
	 * The rest of the range will be spent in "resting" state.
	 * This the "duty cycle" of the jumping animation.
	 */
	public static final float DEFAULT_ANIMATION_DUTY_CYCLE = 0.5f;

	/**
	 * The default duration of a whole jumping animation loop, in milliseconds.
	 */
	public static final int DEFAULT_LOOP_DURATION = 1500;

	private JumpingBeansSpan[] jumpingBeans;
	private WeakReference<TextView> textView;

	private JumpingBeans(JumpingBeansSpan[] beans, TextView textView)
	{
		// Clients will have to use the builder
		this.jumpingBeans = beans;
		this.textView = new WeakReference<TextView>(textView);
	}

	/**
	 * Stops the jumping animation and frees up the animations.
	 */
	public void stopJumping()
	{
		for (JumpingBeansSpan bean : jumpingBeans)
		{
			if (bean != null)
			{
				bean.teardown();
			}
		}

		TextView tv = textView.get();
		if (tv != null)
		{
			CharSequence text = tv.getText();
			if (text instanceof Spanned)
			{
				CharSequence cleanText = removeJumpingBeansSpans((Spanned) text);
				tv.setText(cleanText);
			}
		}
	}

	private static CharSequence removeJumpingBeansSpans(Spanned text)
	{
		SpannableStringBuilder sbb = new SpannableStringBuilder(text.toString());
		Object[] spans = text.getSpans(0, text.length(), Object.class);
		for (Object span : spans)
		{
			if (!(span instanceof JumpingBeansSpan))
			{
				sbb.setSpan(span, text.getSpanStart(span),
						text.getSpanEnd(span), text.getSpanFlags(span));
			}
		}
		return sbb;
	}

	/**
	 * Builder class for {@link com.ctalk.qmqzzs.widget.span.JumpingBeans} objects.
	 * Provides a way to set the fields of a {@link com.ctalk.qmqzzs.widget.span.JumpingBeans} and generate
	 * the desired jumping beans effect. With this builder you can easily append
	 * a Hangouts-style trio of jumping suspension points to any TextView, or
	 * apply the effect to any other subset of a TextView's text.
	 * <p>Example:
	 * <pre class="prettyprint">
	 * JumpingBeans jumpingBeans = new JumpingBeans.Builder()
	 * .appendJumpingDots(myTextView)
	 * .setLoopDuration(1500)
	 * .build();
	 * </pre>
	 */
	public static class Builder
	{

		private int startPos, endPos;
		private float animRange = DEFAULT_ANIMATION_DUTY_CYCLE;
		private int loopDuration = DEFAULT_LOOP_DURATION;
		private int waveCharDelay = -1;
		private CharSequence text;
		private TextView textView;
		private boolean wave;

		/**
		 * Appends three jumping dots to the end of a TextView text.
		 * This implies that the animation will by default be a wave.
		 * If the TextView has no text, the resulting TextView text will
		 * consist of the three dots only.
		 * The TextView text is cached to the current value at
		 * this time and set again in the {@link #build()} method, so any
		 * change to the TextView text done in the meantime will be lost.
		 * This means that <b>you should do all changes to the TextView text
		 * <i>before</i> you begin using this builder.</b>
		 * Call the {@link #build()} method once you're done to get the
		 * resulting {@link com.ctalk.qmqzzs.widget.span.JumpingBeans}.
		 * @param textView The TextView to append the dots to
		 * @see #setIsWave(boolean)
		 */
		public Builder appendJumpingDots(TextView textView)
		{
			if (textView == null)
			{
				throw new NullPointerException("The textView must not be null");
			}

			CharSequence text = !TextUtils.isEmpty(textView.getText()) ? textView.getText() : "";
			if (text.length() > 0 && text.subSequence(text.length() - 1, text.length()).equals("…"))
			{
				text = text.subSequence(0, text.length() - 1);
			}

			if (text.length() < 3 || !TextUtils.equals(text.subSequence(text.length() - 3, text.length()), "..."))
			{
				text = new SpannableStringBuilder(text).append("...");  // Preserve spans in original text
			}

			this.text = text;
			this.wave = true;
			this.textView = textView;
			this.startPos = this.text.length() - 3;
			this.endPos = this.text.length();
			return this;
		}

		/**
		 * Appends three jumping dots to the end of a TextView text.
		 * This implies that the animation will by default be a wave.
		 * If the TextView has no text, the resulting TextView text will
		 * consist of the three dots only.
		 * The TextView text is cached to the current value at
		 * this time and set again in the {@link #build()} method, so any
		 * change to the TextView text done in the meantime will be lost.
		 * This means that <b>you should do all changes to the TextView text
		 * <i>before</i> you begin using this builder.</b>
		 * Call the {@link #build()} method once you're done to get the
		 * resulting {@link com.ctalk.qmqzzs.widget.span.JumpingBeans}.
		 * @param textView The TextView whose text is to be animated
		 * @param startPos The position of the first character to animate
		 * @param endPos   The position after the one the animated range ends at
		 *                 (just like in String#substring())
		 * @see #setIsWave(boolean)
		 */
		public Builder makeTextJump(TextView textView, int startPos, int endPos)
		{
			if (textView == null || textView.getText() == null)
			{
				throw new NullPointerException("The textView and its text must not be null");
			}

			if (endPos < startPos)
			{
				throw new IllegalArgumentException("The start position must be smaller than the end position");
			}

			if (startPos < 0)
			{
				throw new IndexOutOfBoundsException("The start position must be non-negative");
			}

			this.text = textView.getText();
			if (endPos > text.length())
			{
				throw new IndexOutOfBoundsException("The end position must be smaller than the text length");
			}

			this.wave = true;
			this.textView = textView;
			this.startPos = startPos;
			this.endPos = endPos;
			return this;
		}

		/**
		 * Sets the fraction of the animation loop time spent actually animating.
		 * The rest of the time will be spent "resting".
		 * The default value is
		 * {@link com.ctalk.qmqzzs.widget.span.JumpingBeans#DEFAULT_ANIMATION_DUTY_CYCLE}.
		 * @param animatedRange The fraction of the animation loop time spent
		 *                      actually animating the characters
		 */
		public Builder setAnimatedDutyCycle(float animatedRange)
		{
			if (animatedRange <= 0f || animatedRange > 1f)
			{
				throw new IllegalArgumentException("The animated range must be in the (0, 1] range");
			}
			this.animRange = animatedRange;
			return this;
		}

		/**
		 * Sets the jumping loop duration. The default value is
		 * {@link com.ctalk.qmqzzs.widget.span.JumpingBeans#DEFAULT_LOOP_DURATION}.
		 * @param loopDuration The jumping animation loop duration, in milliseconds
		 */
		public Builder setLoopDuration(int loopDuration)
		{
			if (loopDuration < 1)
			{
				throw new IllegalArgumentException("The loop duration must be bigger than zero");
			}
			this.loopDuration = loopDuration;
			return this;
		}

		/**
		 * Sets the delay for starting the animation of every single dot over the
		 * start of the previous one, in milliseconds. The default value is
		 * the loop length divided by three times the number of character animated
		 * by this instance of JumpingBeans.
		 * Only has a meaning when the animation is a wave.
		 * @param waveCharOffset The start delay for the animation of every single
		 *                       character over the previous one, in milliseconds
		 * @see #setIsWave(boolean)
		 */
		public Builder setWavePerCharDelay(int waveCharOffset)
		{
			if (waveCharOffset < 0)
			{
				throw new IllegalArgumentException("The wave char offset must be non-negative");
			}
			this.waveCharDelay = waveCharOffset;
			return this;
		}

		/**
		 * Sets a flag that determines if the characters will jump in a wave
		 * (i.e., with a delay between each other) or all at the same
		 * time.
		 * @param wave If true, the animation is going to be a wave; if
		 *             false, all characters will jump ay the same time
		 * @see #setWavePerCharDelay(int)
		 */
		public Builder setIsWave(boolean wave)
		{
			this.wave = wave;
			return this;
		}

		/**
		 * Combine all of the options that have been set and return a new
		 * {@link com.ctalk.qmqzzs.widget.span.JumpingBeans} instance.
		 * Remember to call the {@link #stopJumping()} method once you're done
		 * using the JumpingBeans (that is, when you detach the TextView from
		 * the view tree, you hide it, or the parent Activity/Fragment goes in
		 * the paused status). This will allow to release the animations and
		 * free up memory and CPU that would be otherwise wasted.
		 */
		public JumpingBeans build()
		{
			SpannableStringBuilder sbb = new SpannableStringBuilder(text);
			JumpingBeansSpan[] jumpingBeans;
			if (!wave)
			{
				jumpingBeans = new JumpingBeansSpan[] { new JumpingBeansSpan(textView, loopDuration, 0, 0, animRange) };
				sbb.setSpan(jumpingBeans[0], startPos, endPos, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
			}
			else
			{
				if (waveCharDelay == -1)
				{
					waveCharDelay = loopDuration / (3 * (endPos - startPos));
				}

				jumpingBeans = new JumpingBeansSpan[endPos - startPos];
				for (int pos = startPos; pos < endPos; pos++)
				{
					JumpingBeansSpan jumpingBean =
							new JumpingBeansSpan(textView, loopDuration, pos - startPos, waveCharDelay, animRange);
					sbb.setSpan(jumpingBean, pos, pos + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
					jumpingBeans[pos - startPos] = jumpingBean;
				}
			}

			textView.setText(sbb);
			return new JumpingBeans(jumpingBeans, textView);
		}
	}
}
